import java.util.LinkedList;


public class Primitive {
	public static final SchavaNumber ZERO = new SchavaNumber(0);
	public static final SchavaNumber ONE = new SchavaNumber(1);

	public static Pair cons(Data a, Data b) {
		return new Pair(a, b);
	}

	public static Pair cons(String a, Data b) {
		return cons(new Atom(a), b);
	}

	public static Pair cons(Data a, String b) {
		return cons(a, new Atom(b));
	}

	public static Pair cons(String a, String b) {
		return cons(new Atom(a), new Atom(b));
	}
	public static Data list(LinkedList<Data> lst) {
		if (lst.isEmpty()) {
			return Evaluator.THE_EMPTY_LIST;
		} else {
			return new Pair(lst.poll(), list(lst));
		}
	}

	public static Data append(Data lstA, Data lstB) {
		if (lstA.isNull()) {
			return lstB;
		} else {
			return cons(lstA.car(), 
					append(lstA.cdr(), lstB));
		}
	}
	
	public static Data map(Data procedure, Data list) {
		if (list.isNull()) {
			return Evaluator.THE_EMPTY_LIST;
		} else {
			return cons(Evaluator.schavaApply(procedure, list.car()),
					map(procedure, list.cdr()));
		}
	}
	
	public static Data cxr(String cxr, Data pair) {
		int len = cxr.length();
		if (len == 0) {
			return pair;
		} else if (cxr.charAt(len - 1) == 'a') {
			return cxr(cxr.substring(0, len - 1), pair.car());
		} else if (cxr.charAt(len - 1) == 'd') {
			return cxr(cxr.substring(0, len - 1), pair.cdr());
		} else {
			throw new SchavaException("Primitive: bad cxr: c" + cxr + "r");
		}
	}

	public static Data applyPrimitive(Procedure procedure, Data arguments) {
		String op = procedure.getValue().toString();
		if (op.equals("+")) {
			return applyPlus(arguments);
		} else if (op.equals("-")) {
			return applyMinus(arguments);
		} else if (op.equals("*")) {
			return applyMul(arguments);
		} else if (op.equals("/")) {
			return applyDiv(arguments);
		} else if (op.equals("=")) {
			return applyEqualsSign(arguments);
		} else if (op.equals("<")) {
			return applyLessThanSign(arguments);
		} else if (op.equals(">")) {
			return applyGreaterThanSign(arguments);
		} else if (op.equals("or")) {
			return applyOr(arguments);
		} else if (op.equals("and")) {
			return applyAnd(arguments);
		} else if (op.equals("equal?")) {
			return applyEqual(arguments);
		} else if (op.equals("eq?")) {
			return applyEq(arguments);
		} else if (op.equals("eq?")) {
			return applyEq(arguments);
		} else if (op.equals("list")) {
			return applyList(arguments);
		} else if (op.equals("car")) {
			return applyCar(arguments);
		} else if (op.equals("cdr")) {
			return applyCdr(arguments);
		} else if (op.equals("cons")) {
			return applyCons(arguments);
		} else if (op.equals("pair?")) {
			return applyPair(arguments);
		} else if (op.equals("atom?")) {
			return applyAtom(arguments);
		} else if (op.equals("null?")) {
			return applyNull(arguments);
		} else if (op.equals("symbol?")) {
			return applySymbol(arguments);
		} else if (op.equals("string?")) {
			return applyString(arguments);
		} else if (op.equals("boolean?")) {
			return applyBoolean(arguments);
		} else if (op.equals("number?")) {
			return applyNumber(arguments);
		} else if (op.equals("procedure?")) {
			return applyProcedure(arguments);
		} else if (op.equals("length")) {
			return applyLength(arguments);
		} else if (op.equals("append")) {
			return applyAppend(arguments);
		} else if (op.equals("read")) {
			return applyRead(arguments);
		} else if (op.equals("newline")) {
			return applyNewline(arguments);
		} else if (op.equals("display")) {
			return applyDisplay(arguments);
		} else if (op.equals("error")) {
			return applyError(arguments);
		} else if (op.equals("quit") 
				|| op.equals("exit")) {
			System.out.println("goodbye");
			System.exit(0);
		}
		throw new SchavaException("Primitive: unknown primitive " + op);
	}


	public static Data applyRead(Data arguments) {
		InputSource lines = new InputSource();
		String line = lines.readLine();
		return new SchavaString(line);
	}

	public static Data applyNewline(Data arguments) {
		System.out.println("");
		return Evaluator.COMMENT;	
	}

	public static Data applyDisplay(Data arguments) {
		try {
			SchavaString message;
			Data car = arguments.car();
			if (car.isNumber()) {
				System.out.print(car.toString());
			} else {
				message = (SchavaString) arguments.car();
				System.out.print(message);
			}
			return Evaluator.COMMENT;
		} catch (ClassCastException e) {
			throw new SchavaException("Primitive: display not string " + arguments.toString());
		}
	}
	
	
	
	public static SchavaNumber applyLength(Data arguments) {
		if (Primitive.length(arguments) != 1) {
			throw new SchavaException("Primitive: length expects one argument, found " 
					+ Primitive.length(arguments));
		}
		return new SchavaNumber(Primitive.length(arguments.car()));
	}

	public static Data applyAppend(Data arguments) {
		if (Primitive.length(arguments) != 2) {
			throw new SchavaException("Primitive: append expects two arguments, found " 
					+ Primitive.length(arguments));
		}
		return Primitive.append(arguments.car(), arguments.cdr().car());
	}

	public static SchavaBoolean applySymbol(Data arguments) {
		if (Primitive.length(arguments) != 1) {
			throw new SchavaException("Primitive: symbol? expects one argument, found " 
					+ Primitive.length(arguments));
		}
		if (arguments.car().isSymbol()) {
			return Evaluator.SCHAVA_TRUE;
		} else {
			return Evaluator.SCHAVA_FALSE;
		}
	}

	public static SchavaBoolean applyString(Data arguments) {
		if (Primitive.length(arguments) != 1) {
			throw new SchavaException("Primitive: string? expects one argument, found " 
					+ Primitive.length(arguments));
		}
		if (arguments.car().isString()) {
			return Evaluator.SCHAVA_TRUE;
		} else {
			return Evaluator.SCHAVA_FALSE;
		}
	}

	public static SchavaBoolean applyBoolean(Data arguments) {
		if (Primitive.length(arguments) != 1) {
			throw new SchavaException("Primitive: boolean? expects one argument, found " 
					+ Primitive.length(arguments));
		}
		if (arguments.car().isBoolean()) {
			return Evaluator.SCHAVA_TRUE;
		} else {
			return Evaluator.SCHAVA_FALSE;
		}
	}

	public static SchavaBoolean applyNumber(Data arguments) {
		if (Primitive.length(arguments) != 1) {
			throw new SchavaException("Primitive: number? expects one argument, found " 
					+ Primitive.length(arguments));
		}
		if (arguments.car().isNumber()) {
			return Evaluator.SCHAVA_TRUE;
		} else {
			return Evaluator.SCHAVA_FALSE;
		}
	}

	public static SchavaBoolean applyProcedure(Data arguments) {
		if (Primitive.length(arguments) != 1) {
			throw new SchavaException("Primitive: procedure? expects one argument, found " 
					+ Primitive.length(arguments));
		}
		if (arguments.car().isProcedure()) {
			return Evaluator.SCHAVA_TRUE;
		} else {
			return Evaluator.SCHAVA_FALSE;
		}
	}

	public static SchavaBoolean applyError(Data arguments) {
		if (Primitive.length(arguments) != 1) {
			throw new SchavaException("Primitive: error expects one argument, found " 
					+ Primitive.length(arguments));
		}
		throw new SchavaException("Error: " + arguments.car().toString());
	}

	public static LinkedList<Pair> getPrimitives() {
		LinkedList<Pair> pairs = new LinkedList<Pair>();
		pairs.add(cons("+", new Procedure("+")));
		pairs.add(cons("-", new Procedure("-")));
		pairs.add(cons("*", new Procedure("*")));
		pairs.add(cons("/", new Procedure("/")));
		pairs.add(cons("=", new Procedure("=")));
		pairs.add(cons("<", new Procedure("<")));
		pairs.add(cons(">", new Procedure(">")));
		pairs.add(cons("or", new Procedure("or")));
		pairs.add(cons("and", new Procedure("and")));
		pairs.add(cons("equal?", new Procedure("equal?")));
		pairs.add(cons("eq?", new Procedure("eq?")));
		pairs.add(cons("being", new Procedure("begin")));
		pairs.add(cons("define", new Procedure("define")));
		pairs.add(cons("let", new Procedure("let")));
		pairs.add(cons("cond", new Procedure("cond")));		
		pairs.add(cons("set!", new Procedure("set!")));
		pairs.add(cons("list", new Procedure("list")));
		
		pairs.add(cons("car", new Procedure("car")));
		pairs.add(cons("cdr", new Procedure("cdr")));
		
//		pairs.add(cons("caar", new Procedure("caar")));
//		pairs.add(cons("cadr", new Procedure("cadr")));
//		pairs.add(cons("cdar", new Procedure("cdar")));
//		pairs.add(cons("cddr", new Procedure("cddr")));
//		
//		pairs.add(cons("caaaar", new Procedure("caaar")));
//		pairs.add(cons("caaadr", new Procedure("caaadr")));
//		pairs.add(cons("cadar", new Procedure("caddar")));
//		pairs.add(cons("caddr", new Procedure("caddr")));
		
		pairs.add(cons("cons", new Procedure("cons")));
		pairs.add(cons("pair?", new Procedure("pair?")));
		pairs.add(cons("atom?", new Procedure("atom?")));
		pairs.add(cons("null?", new Procedure("null?")));
		pairs.add(cons("#t", Evaluator.SCHAVA_TRUE));
		pairs.add(cons("#f", Evaluator.SCHAVA_FALSE));
		pairs.add(cons("pi", new Atom(new Double(3.14159))));
		pairs.add(cons("symbol?", new Procedure("symbol?")));
		pairs.add(cons("string?", new Procedure("string?")));
		pairs.add(cons("boolean?", new Procedure("boolean?")));
		pairs.add(cons("number?", new Procedure("number?")));
		pairs.add(cons("procedure?", new Procedure("procedure?")));
		pairs.add(cons("length", new Procedure("length")));
		pairs.add(cons("apply", new Procedure("apply")));
		pairs.add(cons("append", new Procedure("append")));
		pairs.add(cons("read", new Procedure("read")));
		pairs.add(cons("newline", new Procedure("newline")));
		pairs.add(cons("display", new Procedure("display")));
		pairs.add(cons("quote", new Procedure("quote")));
		pairs.add(cons("error", new Procedure("error")));
		pairs.add(cons("quit", new Procedure("quit")));
		pairs.add(cons("exit", new Procedure("exit")));

		Data notData = Parser.parse("(lambda (x) (if x #f #t))");
		Procedure not = (Procedure) Evaluator.eval(notData.car(), Evaluator.THE_GLOBAL_ENVIRONMENT);
		pairs.add(cons("not",not));
		return pairs;
	}


	private static SchavaBoolean applyNull(Data arguments) {
		if (arguments.car().isNull()) {
			return Evaluator.SCHAVA_TRUE;
		} else {
			return Evaluator.SCHAVA_FALSE;
		}
	}


	private static SchavaBoolean applyAtom(Data arguments) {
		if (arguments.car().isAtom()) {
			return Evaluator.SCHAVA_TRUE;
		} else {
			return Evaluator.SCHAVA_FALSE;
		}
	}


	private static SchavaBoolean applyPair(Data arguments) {
		if (arguments.car().isPair()) {
			return Evaluator.SCHAVA_TRUE;
		} else {
			return Evaluator.SCHAVA_FALSE;
		}
	}

	private static Data applyCons(Data arguments) {
		if (length(arguments) == 2) {
			return cons(arguments.car(), arguments.cdr().car());
		}
		throw new SchavaException("cons: expects two arguments");
	}

	private static Data applyCdr(Data arguments) {
		if (length(arguments) == 1) {
			return arguments.car().cdr();
		}
		throw new SchavaException("cdr: expects one argument");
	}

	private static Data applyCar(Data arguments) {
		if (length(arguments) == 1) {
			return arguments.car().car();
		}
		throw new SchavaException("car: expects one argument");
	}

	private static Data applyList(Data arguments) {
		if (arguments.isNull()) {
			return Evaluator.THE_EMPTY_LIST;
		} else {
			return new Pair(arguments.car(), applyList(arguments.cdr())); 
		}
	}

	private static SchavaBoolean applyEqual(Data arguments) {
		if (length(arguments) == 1) {
			return Evaluator.SCHAVA_TRUE;
		} else if (arguments.car().equals(arguments.cdr().car())) {
			return applyEqual(arguments.cdr()); 
		} else {
			return Evaluator.SCHAVA_FALSE;
		}
	}

	private static SchavaBoolean applyEq(Data arguments) {
		if (length(arguments) == 1) {
			return Evaluator.SCHAVA_TRUE;
		} else if (arguments.car() == arguments.cdr().car()) {
			return applyEqual(arguments.cdr()); 
		} else {
			return Evaluator.SCHAVA_FALSE;
		}
	}

	private static SchavaBoolean applyEqualsSign(Data arguments) {
		if (length(arguments) == 1) {
			return Evaluator.SCHAVA_TRUE;
		} else if (arguments.car().toString().equals(arguments.cdr().car().toString())) {
			return applyEqualsSign(arguments.cdr()); 
		} else {
			return Evaluator.SCHAVA_FALSE;
		}
	}

	private static SchavaBoolean applyLessThanSign(Data arguments) {
		if (length(arguments) == 1) {
			return Evaluator.SCHAVA_TRUE;
		} else if (arguments.car().compareTo(arguments.cdr().car()) < 0) {
			return applyLessThanSign(arguments.cdr()); 
		} else {
			return Evaluator.SCHAVA_FALSE;
		}
	}

	private static SchavaBoolean applyGreaterThanSign(Data arguments) {
		if (length(arguments) == 1) {
			return Evaluator.SCHAVA_TRUE;
		} else if (arguments.car().toString().compareTo(arguments.cdr().car().toString()) > 0) {
			return applyGreaterThanSign(arguments.cdr()); 
		} else {
			return Evaluator.SCHAVA_FALSE;
		}
	}

	private static SchavaBoolean applyOr(Data arguments) {
		if (length(arguments) > 0) {
			if (arguments.car().isTrue()) {
				return Evaluator.SCHAVA_TRUE;
			} else {
				return applyOr(arguments.cdr());
			}
		} else {
			return Evaluator.SCHAVA_FALSE;
		}
	}

	private static SchavaBoolean applyAnd(Data arguments) {
		if (length(arguments) > 0) {
			if (arguments.car().isTrue()) {
				return applyAnd(arguments.cdr());
			} else {
				return Evaluator.SCHAVA_FALSE;
			}
		} else {
			return Evaluator.SCHAVA_TRUE;
		}
	}

	private static SchavaNumber applyPlus(Data arguments) {
		if (arguments.isNull()) {
			return ZERO;
		} else {
			try {
				SchavaNumber first = (SchavaNumber)arguments.car();
				SchavaNumber rest = applyPlus(arguments.cdr());
				return first.add(rest);
			} catch (ClassCastException e) {
				throw new SchavaException("Primitive: applyPlus arguments are not numbers " + arguments.toString());
			}
		}
	}

	private static SchavaNumber applyMinus(Data arguments) {
		try {
			if (length(arguments) == 1) {
				return ZERO.sub((SchavaNumber)arguments.car());
			} else {
				SchavaNumber first = (SchavaNumber)arguments.car();
				SchavaNumber rest = applyPlus(arguments.cdr());
				return first.sub(rest);
			}
		} catch (ClassCastException e) {
			throw new SchavaException("Primitive: applyMinus arguments are not numbers " + arguments.toString());
		}
	}

	private static SchavaNumber applyMul(Data arguments) {
		try {
			if (arguments.isNull()) {
				return ONE;
			} else {
				SchavaNumber first = (SchavaNumber)arguments.car();
				SchavaNumber rest = applyMul(arguments.cdr());
				return first.mul(rest); 
			}
		} catch (ClassCastException e) {
			throw new SchavaException("Primitive: applyMul arguments are not numbers " + arguments.toString());
		}
	}

	private static SchavaNumber applyDiv(Data arguments) {
		try {
			if (length(arguments) == 1) {
				return ONE.div(applyMul(arguments));
			} else {

				SchavaNumber first = (SchavaNumber)arguments.car();
				SchavaNumber rest = applyMul(arguments.cdr());
				return first.div(rest);
			}
		} catch (ClassCastException e) {
			throw new SchavaException("Primitive: applyDiv arguements are not numbers " + arguments.toString());
		}
	}




	public static int length(Data lst) {
		if (lst.equals(Evaluator.THE_EMPTY_LIST)) {
			return 0;
		} else if (lst instanceof Pair) {
			return 1 + length(((Pair)lst).cdr());
		}
		throw new SchavaException("length: not calculable");
	}

	public static int numCars(Data lst) {
		if (lst instanceof Pair) {
			return 1 + numCars(((Pair)lst).cdr());
		} else {
			return 0;
		}
	}
	
	
	/**
	 * (define (<= foo . args) (or (apply < (cons foo args)) (apply = (cons foo args))))
	 */
}
